/**
 * HashMap - HashMap Class for JavaScript
 * @author Ariel Flesler <aflesler@gmail.com>
 * @version 2.4.0
 * Homepage: https://github.com/flesler/hashmap
 */

(function(factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof module === 'object') {
        // Node js environment
        var HashMap = module.exports = factory();
        // Keep it backwards compatible
        HashMap.HashMap = HashMap;
    } else {
        // Browser globals (this is window)
        this.HashMap = factory();
    }
}(function() {

    function HashMap(other) {
        this.clear();
        switch (arguments.length) {
            case 0: break;
            case 1: {
                if ('length' in other) {
                    // Flatten 2D array to alternating key-value array
                    multi(this, Array.prototype.concat.apply([], other));
                } else { // Assumed to be a HashMap instance
                    this.copy(other);
                }
                break;
            }
            default: multi(this, arguments); break;
        }
    }

    var proto = HashMap.prototype = {
        constructor:HashMap,

        get:function(key) {
            var data = this._data[this.hash(key)];
            return data && data[1];
        },

        set:function(key, value) {
            // Store original key as well (for iteration)
            var hash = this.hash(key);
            if ( !(hash in this._data) ) {
                this.size++;
            }
            this._data[hash] = [key, value];
        },

        multi:function() {
            multi(this, arguments);
        },

        copy:function(other) {
            for (var hash in other._data) {
                if ( !(hash in this._data) ) {
                    this.size++;
                }
                this._data[hash] = other._data[hash];
            }
        },

        has:function(key) {
            return this.hash(key) in this._data;
        },

        search:function(value) {
            for (var key in this._data) {
                if (this._data[key][1] === value) {
                    return this._data[key][0];
                }
            }

            return null;
        },

        delete:function(key) {
            var hash = this.hash(key);
            if ( hash in this._data ) {
                this.size--;
                delete this._data[hash];
            }
        },

        type:function(key) {
            var str = Object.prototype.toString.call(key);
            var type = str.slice(8, -1).toLowerCase();
            // Some browsers yield DOMWindow or Window for null and undefined, works fine on Node
            if (!key && (type === 'domwindow' || type === 'window')) {
                return key + '';
            }
            return type;
        },

        keys:function() {
            var keys = [];
            this.forEach(function(_, key) { keys.push(key); });
            return keys;
        },

        values:function() {
            var values = [];
            this.forEach(function(value) { values.push(value); });
            return values;
        },

        entries:function() {
            var entries = [];
            this.forEach(function(value, key) { entries.push([key, value]); });
            return entries;
        },

        // TODO: This is deprecated and will be deleted in a future version
        count:function() {
            return this.size;
        },

        clear:function() {
            // TODO: Would Object.create(null) make any difference
            this._data = {};
            this.size = 0;
        },

        clone:function() {
            return new HashMap(this);
        },

        hash:function(key) {
            switch (this.type(key)) {
                case 'undefined':
                case 'null':
                case 'boolean':
                case 'number':
                case 'regexp':
                    return key + '';

                case 'date':
                    return '♣' + key.getTime();

                case 'string':
                    return '♠' + key;

                case 'array':
                    var hashes = [];
                    for (var i = 0; i < key.length; i++) {
                        hashes[i] = this.hash(key[i]);
                    }
                    return '♥' + hashes.join('⁞');

                default:
                    // TODO: Don't use expandos when Object.defineProperty is not available?
                    if (!key.hasOwnProperty('_hmuid_')) {
                        key._hmuid_ = ++HashMap.uid;
                        hide(key, '_hmuid_');
                    }

                    return '♦' + key._hmuid_;
            }
        },

        forEach:function(func, ctx) {
            for (var key in this._data) {
                var data = this._data[key];
                func.call(ctx || this, data[1], data[0]);
            }
        }
    };

    HashMap.uid = 0;

    // Iterator protocol for ES6
    if (typeof Symbol !== 'undefined' && typeof Symbol.iterator !== 'undefined') {
        proto[Symbol.iterator] = function() {
            var entries = this.entries();
            var i = 0;
            return {
                next:function() {
                    if (i === entries.length) { return { done: true }; }
                    var currentEntry = entries[i++];
                    return {
                        value: { key: currentEntry[0], value: currentEntry[1] },
                        done: false
                    };
                }
            };
        };
    }

    //- Add chaining to all methods that don't return something

    ['set','multi','copy','delete','clear','forEach'].forEach(function(method) {
        var fn = proto[method];
        proto[method] = function() {
            fn.apply(this, arguments);
            return this;
        };
    });

    //- Backwards compatibility

    // TODO: remove() is deprecated and will be deleted in a future version
    HashMap.prototype.remove = HashMap.prototype.delete;

    //- Utils

    function multi(map, args) {
        for (var i = 0; i < args.length; i += 2) {
            map.set(args[i], args[i+1]);
        }
    }

    function hide(obj, prop) {
        // Make non iterable if supported
        if (Object.defineProperty) {
            Object.defineProperty(obj, prop, {enumerable:false});
        }
    }

    return HashMap;
}));